Add File / FileReader polyfill#169
Conversation
There was a problem hiding this comment.
Pull request overview
This PR moves File / FileReader support from a BabylonNative Playground-only JS shim into JsRuntimeHost as a native C++ polyfill, so all JsRuntimeHost consumers get the WHATWG-style APIs alongside existing polyfills (Blob, URL, XHR, etc.).
Changes:
- Add a new
Polyfills/File/C++ target implementingFileandFileReader(including event handler slots + EventTarget-style listeners). - Wire the polyfill into the build via a new
JSRUNTIMEHOST_POLYFILL_FILECMake option (ON by default) and link it into unit tests. - Add new Mocha unit tests covering
FileandFileReaderbehaviors (construction, reads, events, abort, and a JSC regression aroundObject.prototypepollution).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| CMakeLists.txt | Adds JSRUNTIMEHOST_POLYFILL_FILE option (ON by default). |
| Polyfills/CMakeLists.txt | Conditionally includes the new File polyfill subdirectory. |
| Polyfills/File/CMakeLists.txt | Defines the File polyfill library target and its sources. |
| Polyfills/File/Include/Babylon/Polyfills/File.h | Public API entrypoint for initializing the polyfill. |
| Polyfills/File/Readme.md | Documents behavior and prerequisites (Blob must be initialized first). |
| Polyfills/File/Source/File.h | Declares the internal File ObjectWrap implementation. |
| Polyfills/File/Source/File.cpp | Implements File over the existing Blob polyfill and initializes FileReader. |
| Polyfills/File/Source/FileReader.h | Declares the internal FileReader ObjectWrap implementation. |
| Polyfills/File/Source/FileReader.cpp | Implements async reads, base64 DataURL generation, events, and abort logic. |
| Tests/UnitTests/CMakeLists.txt | Links the new File target into the UnitTests executable. |
| Tests/UnitTests/Shared/Shared.cpp | Initializes the File polyfill in the unit test JS environment. |
| Tests/UnitTests/Scripts/tests.ts | Adds new Mocha tests for File and FileReader, including a JSC regression canary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6f8a827 to
67f9f5c
Compare
fa84e76 to
52f6338
Compare
|
@bghgary friendly ping — this is the upstream half of BabylonJS/BabylonNative#1706 (File / FileReader polyfill, which re-enables 19 GLTF/OBJ serializer tests in BN). Same flow as #171 (TextEncoder) which you helped me close out earlier today: the polyfill was moved here from the BN PR per your review ("this should be a proper C++ polyfill under State just refreshed:
Once this lands I'll bump BN #1706's pin back to |
ryantrem
left a comment
There was a problem hiding this comment.
Similar to my comment on the other PR, we may want to do a minimal implementation of the File polyfill that is just enough to support BJS usage (not sure if that is already the case).
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
|
@ryantrem on the minimization point — I surveyed Babylon.js usage and the only thing that wasn't already minimal is
Dropping Full set of changes in 4480ad8:
Local Win32 Chakra |
|
CI on The prototype-chain wire-up for
Ball back in your court for re-review. |
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
Recommend landing #172 first. The JSC napi-shim bug it describes is the root cause of several workarounds in this PR — JS_PROTOTYPE_CHAIN_SHIM collapses to one direct setPrototypeOf call, the Napi::Eval / env.RunScript dispatch goes away, and the 14-line FileReader dual-exposure comment shrinks to one line. Quite a bit of code evaporates before merge if #172 lands first.
Other concerns inline.
|
Filed #174 with the JSC napi shim fix for #172. It replaces the old Once #174 lands, I'll rebase this branch onto main and:
Will respond on the Re: splitting |
|
Update: closing #174 without merging — the So #169 keeps its JS-shim wire-up (
|
…abylonJS#169 File.cpp - Drop the misleading BABYLON_POLYFILL_USE_NAPI_JSI_EVAL macro and the __has_include(<napi/env.h>) guard. <napi/env.h> exists on every backend (V8, Chakra, JSC, JSI) and is pulled in transitively via <Babylon/Polyfills/File.h>, so the include guard always succeeded and the macro name implied a JSI-only path that doesn't exist — Napi::Eval is declared on all four backends (the Shared N-API impl in env.cc is a thin wrapper around env.RunScript). Call Napi::Eval directly. - Reorder Initialize: check "already provided" (cheap no-op, common path on platforms with a native File) before probing for Blob. - Throw Napi::Error on missing Blob instead of silently bailing. Consumers that wire up the File polyfill expect it to be installed; silent failures are hard to debug. FileReader.cpp - MakeEvent: replace the dangling "JS polyfill that this C++ implementation replaces" reference (no such JS polyfill exists in this PR) with a one-line description of the actual ProgressEvent contract. - DefineClass: collapse the 14-line dual-StaticValue/InstanceValue comment to one line pointing at JsRH#173. Per BabylonJS#173 this is the correct WHATWG IDL `const` member exposure pattern, not a workaround that needs in-line justification. tests.ts - Re-point the Chakra throw-from-constructor TODO at JsRH#175 (filed today) so the disabled "throws when fewer than 2 arguments are passed" test can be re-enabled atomically when that shim limitation is fixed. No behavior change; pure cleanup. Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed Changes in this commit:
Still to come (separate commits for review-clarity, in priority order):
#172 / #174 update: as noted in the follow-up comment on #174, the proper JSC |
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
Please merge main to pick up #177 + #179.
One related cleanup outside this PR's diff — Tests/UnitTests/Scripts/tests.ts lines 1292–1297 still has the TODO(#178) / isPrototypeOf workaround that #177 introduced for Chakra. #179 retired it; after merging main, restore the depth-1 check:
- // TODO(#178): Chakra's napi_wrap interposes a hidden external object
- // into the instance's prototype chain, so Object.getPrototypeOf(blob)
- // !== Blob.prototype at depth 1 on Chakra. Use isPrototypeOf (chain
- // walk) until that is fixed.
- expect(Blob.prototype.isPrototypeOf(blob)).to.equal(true);
+ expect(Object.getPrototypeOf(blob)).to.equal(Blob.prototype);Implements the WHATWG File and FileReader web APIs as a native
polyfill, alongside the existing Blob polyfill that File delegates to
for byte storage.
* `Polyfills/File/` -- new polyfill target, gated on a new
`JSRUNTIMEHOST_POLYFILL_FILE` CMake option (ON by default).
* `Tests/UnitTests/Scripts/tests.ts` -- 28 new Mocha tests
(13 File + 15 FileReader), including a regression canary for
Object.prototype pollution by `FileReader.EMPTY/LOADING/DONE`.
Notable implementation details
------------------------------
* FileReader registers its `EMPTY/LOADING/DONE` constants via
`StaticValue` and `InstanceValue` descriptors inside the
`DefineClass` property list. On JavaScriptCore the napi shim's
`ConstructorInfo::Create` defaults the constructor's `.prototype`
to `Object.prototype`, so setting these via
`func.Get("prototype").Set(...)` would pollute every plain object
in the runtime.
* The Blob-dependency guard in `File::Initialize` uses `IsUndefined()`
rather than `IsFunction()`. Some JSC builds (notably
`libjavascriptcoregtk` on Linux) classify constructors created via
`JSObjectMakeConstructor` as `typeof 'object'`, not `'function'`,
so `napi_typeof` returns `napi_object` for them and an
`IsFunction()` guard would silently early-return on those engines.
`IsUndefined()` matches the guard used by Blob and works on every
engine we ship.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on Chakra ThrowAsJavaScriptException avoids the Chakra heap-corruption on throw-from-constructor, but JSI's napi shim implements `Error::ThrowAsJavaScriptException` as a no-op that only stores the error in `env->last_exception` without raising it. The script then continues normally with an incompletely-initialized File wrapper, the `expect(...).to.throw()` matcher sees no JS error, and the unhandled exception is later reported through AppRuntime's UnhandledExceptionHandler, failing the test harness exit code. Use the JRH-wide convention `throw Napi::TypeError::New(env, msg)` (matches Blob, XHR, URL, AbortSignal, TextDecoder, FileReader). On engines whose Node-API shim properly translates a C++ throw into a JS exception at the ObjectWrap construction boundary (V8 / JSC / JSI), this propagates as expected. Chakra cannot handle throwing from a constructor — there are five pre-existing TODOs in tests.ts noting this same limitation for URL parse() error cases. Apply the same workaround here: keep the WebIDL-conformant throw in C++, but comment out the test that exercises it (it would corrupt the Chakra heap on every CI run). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…_ptr trick Addresses @ryantrem review on BabylonJS#169 (review id 4412098338): * Drop FileReader.readAsBinaryString and its supporting code paths. BJS has zero call sites (searched core, dev/, tools/). Removing it also eliminates the deferred Latin-1/UTF-8 follow-up that previously crashed Chakra during a combined-edit attempt. * Wire File.prototype to inherit from Blob.prototype via Object.setPrototypeOf in File::Initialize. Babylon.js core branches on `instanceof Blob` in fileTools, Offline/database, abstractEngine, and thinNativeEngine; without inheritance those checks silently fail for File inputs and the wrong branch is taken. Internal m_blob composition stays as the implementation detail; only the JS-visible prototype chain is wired. New test asserts `new File(...) instanceof Blob === true`. * Replace shared_ptr<ObjectReference>-in-lambda trick with a member Napi::ObjectReference m_selfRef anchored across the in-flight read. Matches the member-slot pattern used by WebSocket/XHR in this repo. Lambdas now only capture POD + this, so they remain copyable for jsi::Function's std::function-style callable slot. Every terminal path (load, error, abort) resets m_selfRef to break the self-cycle. Each lambda also guards on m_readId before dereferencing m_selfRef to handle the abort-then-late-resolve race.
CI failed on JSC engines (Ubuntu_gcc, macOS, iOS, Android_JSC) with
'[Uncaught Error] setPrototypeOf@[native code]' during JS env init.
Root cause: the previous commit called
\Object.setPrototypeOf(func.Get('prototype'), Blob.prototype)\. On JSC,
the napi-defined class's \.prototype\ JS property points to
Object.prototype (per JSObjectMakeConstructor semantics; same quirk the
FileReader constants block documents). setPrototypeOf on Object.prototype
throws TypeError because Object.prototype's [[Prototype]] is immutable.
Fix: instantiate a throwaway File, fetch its real prototype via
Object.getPrototypeOf, and set THAT prototype's prototype to
Blob.prototype. On V8 / Chakra the real prototype is also
func.prototype, so this is correct everywhere. The temp instance is
GC-eligible immediately after.
Each call is guarded with IsExceptionPending + GetAndClearPendingException
so that if a non-Chromium JSC build still rejects setPrototypeOf on the
napi-internal prototype, Initialize stays best-effort: instances won't be
Blob subtypes on that engine but the rest of the polyfill still
installs and the JS env starts cleanly.
Commit ac19d77 swapped the direct setPrototypeOf for a temp-instance trick to fix JSC, but that broke Chakra (Win32_x64_Chakra and Win32_x86_Chakra now report `file instanceof Blob === false`). Restore the direct call (which previously passed on V8 and Chakra), verify the chain via a probe instance, and only fall back to the temp-instance approach when the chain isn't wired up. That recovers JSC (where `func.prototype` aliases `Object.prototype`) without regressing the engines where the direct call was already correct.
Commit 9e7b333 (C++ dual-path direct -> temp-instance fallback) passed Chakra but regressed JSC: on JSC, setPrototypeOf on Object.prototype throws TypeError that escapes the napi shim as an [Uncaught Error] instead of being capturable via IsExceptionPending. Move both setPrototypeOf calls into a single JS shim wrapped in try/catch. JS-level try/catch reliably traps the TypeError on every engine, the direct path takes care of V8/Chakra, and the probe path (instanceof Blob check + getPrototypeOf + setPrototypeOf on the real napi-internal prototype) takes care of JSC.
…abylonJS#169 File.cpp - Drop the misleading BABYLON_POLYFILL_USE_NAPI_JSI_EVAL macro and the __has_include(<napi/env.h>) guard. <napi/env.h> exists on every backend (V8, Chakra, JSC, JSI) and is pulled in transitively via <Babylon/Polyfills/File.h>, so the include guard always succeeded and the macro name implied a JSI-only path that doesn't exist — Napi::Eval is declared on all four backends (the Shared N-API impl in env.cc is a thin wrapper around env.RunScript). Call Napi::Eval directly. - Reorder Initialize: check "already provided" (cheap no-op, common path on platforms with a native File) before probing for Blob. - Throw Napi::Error on missing Blob instead of silently bailing. Consumers that wire up the File polyfill expect it to be installed; silent failures are hard to debug. FileReader.cpp - MakeEvent: replace the dangling "JS polyfill that this C++ implementation replaces" reference (no such JS polyfill exists in this PR) with a one-line description of the actual ProgressEvent contract. - DefineClass: collapse the 14-line dual-StaticValue/InstanceValue comment to one line pointing at JsRH#173. Per BabylonJS#173 this is the correct WHATWG IDL `const` member exposure pattern, not a workaround that needs in-line justification. tests.ts - Re-point the Chakra throw-from-constructor TODO at JsRH#175 (filed today) so the disabled "throws when fewer than 2 arguments are passed" test can be re-enabled atomically when that shim limitation is fixed. No behavior change; pure cleanup. Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onJS#177 landed BabylonJS#177 fixed the JSC napi shim's Object.prototype pollution by passing the per-class JSClassRef to JSObjectMakeConstructor, so napi-defined classes now get a real per-class .prototype object instead of aliasing the global Object.prototype. That removes the entire reason for the JS_PROTOTYPE_CHAIN_SHIM and the Napi::Eval / try/catch dance: we can wire File.prototype's [[Prototype]] to Blob.prototype with a direct Object.setPrototypeOf call. Drops ~50 lines of explanatory comment + shim + Eval-with-IsExceptionPending guard in File::Initialize down to a 4-line napi call. `file instanceof Blob` regression coverage remains in tests.ts:1564 and BabylonJS#177's `describe("napi class prototype isolation (BabylonJS#172)")` block at tests.ts:1271 (uses Blob — and File extends Blob — to assert that napi-defined classes don't share Object.prototype). Local Chakra build clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ment Per bghgary's review nit: the "BabylonJS#177 fixed JSC .prototype pollution" context is now in main's git history, doesn't need to live in the comment block. Comment now focuses on WHY we wire File→Blob, not on which past bug enabled the wiring to be one-liner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0bff942 to
fe457e5
Compare
|
Rebased onto current Both asks from your latest review addressed:
Rebase was clean — 8 commits replayed cleanly on top of |
…eAccessor readyState/result/error are WHATWG readonly attributes and the on* slots are EventHandler IDL attributes; both should be prototype accessors, not writable enumerable own data properties stamped onto every instance. - readyState/result/error -> InstanceAccessor getters reading m_readyState / m_result / m_error. State-machine checks now read the C++ members directly, so JS can no longer tamper with them by overwriting the property. - on* handlers -> a single InstanceAccessor get/set pair whose accessor data carries the event type, backed by a Napi::FunctionReference map. Dispatch consults the map instead of the on-prefixed instance property. - Constructor no longer stamps any instance properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…impl Per review, avoid carrying a third hand-rolled base64 encoder. Promote the header-only azawadzki/base-n (already used by BabylonNative) into JsRuntimeHost as a FetchContent INTERFACE library, gated on JSRUNTIMEHOST_POLYFILL_FILE, and link it PRIVATE into the File polyfill. base-n's encode_b64 emits unpadded base64, so EncodeBase64 now just delegates to it and appends the RFC 4648 '=' padding for the final partial group. This keeps data: URL output byte-for-byte identical (e.g. "Hello" -> "SGVsbG8="). Pinned to the same commit BabylonNative uses (7573e77c). BN's direct base-n dependency can be dropped as a follow-up once it consumes JRH's. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the remaining review asks (pushed
All 18 review threads are now resolved. |
napi_create_reference only accepts heap values (object/function/symbol) on the V8/JSC N-API backends, so storing a primitive string result (readAsText/readAsDataURL) in a Napi::Reference<Value> threw there and the load/loadend events never fired (Chakra's shim tolerated it, which is why this passed locally). Box result/error as properties on a persistent holder object instead; referencing the holder object is valid for every engine while keeping the state C++-owned and tamper-proof. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI on Verified green (all FileReader tests pass — 179 passing):
The only red job is |
Replace the m_selfRef member self-reference with a shared_ptr<ObjectReference> captured by both promise reactions. The spec requires an in-flight read to keep the FileReader alive even if script drops its reference; this anchors the wrapper externally (owned by the lambdas) for exactly the read's duration. When the promise settles and the engine releases the reactions, the last shared_ptr copy drops and the anchor is released automatically. This drops the member self-cycle and the manual m_selfRef.Reset() on every terminal path, and removes the dead-this race the self-anchor left on the abort-then-drop path: the wrapper is now guaranteed alive whenever a reaction runs, so reading m_readId and calling anchor->Value() are always safe. m_readId is retained solely as the abort/restart guard. Matches the externally-anchored, lambda-owned-state convention used by the WebSocket polyfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@bghgary thanks for the careful review — all of your inline concerns are addressed in 61d9924:
I also refreshed the PR description to match the current implementation (it had drifted): C++-backed readonly accessors for Verified Win32-x64 Chakra Debug green (180 JS + native gtests), and full CI is green across all engines/platforms. PTAL. |
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
LGTM. All concerns addressed; the FileReader in-flight lifecycle/UAF fix is verified in 61d9924 — shared_ptr<ObjectReference> anchor captured by both promise reactions, no self-ref member, no manual Reset().
### Problem When JsRuntimeHost is embedded via FetchContent in a parent project that already declares its own `base-n` INTERFACE target — e.g. **BabylonNative**, which uses base-n for its Window/Canvas polyfills — configuring fails: ``` CMake Error at .../jsruntimehost-src/CMakeLists.txt:150 (add_library): add_library cannot create target "base-n" because another target with the same name already exists. The existing target is an interface library created in source directory ".../Dependencies". ``` This surfaced now because the merged File / FileReader polyfill (#169) switched its base64 encoder over to the `base-n` dependency, and BabylonNative declares `base-n` before it fetches JsRuntimeHost. ### Fix Guard the `base-n` block with `NOT TARGET base-n` so that when a parent project already provides the target, JsRuntimeHost adopts it instead of redeclaring. This is the standard CMake subproject pattern and mirrors BabylonNative's own `if(NOT TARGET glslang)` guards. Standalone JsRuntimeHost builds are unaffected (no pre-existing target → block runs as before). ### Validation - Standalone `NAPI_JAVASCRIPT_ENGINE=Chakra` configure + `UnitTests` build: green, File polyfill + base-n link as before. - Unblocks BabylonJS/BabylonNative#1706 (the consuming PR that re-pins to merged main). Co-authored-by: Branimir Karadzic <branimirkaradzic@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The File polyfill in BabylonNative currently lives as a JS shim (
Apps/Playground/Scripts/file_polyfill.js, ~308 lines) that isLoadScript-ed from the Playground'sAppContext(see BabylonJS/BabylonNative#1706). Per @bghgary's review on that PR, it should live in JsRuntimeHost alongside Blob, URL, WebSocket, XMLHttpRequest, TextDecoder, AbortController, etc., so every JsRuntimeHost consumer -- not only the BabylonNative Playground -- getsFileandFileReader.This PR adds a native C++
Polyfills/File/target that implements the WHATWGFileandFileReaderweb APIs, plus 29 new Mocha unit tests (15 File + 14 FileReader) underTests/UnitTests/Scripts/tests.ts.Surface area
File(parts, name, options)constructor.Fileinstance accessors:size,type,name,lastModified.Fileinstance methods:arrayBuffer(),text(),bytes().File extends Blob: the prototype chain is wired sonew File(...) instanceof Blob === true(Babylon.js core branches oninstanceof Blobin fileTools, Offline/database, abstractEngine, thinNativeEngine).FileReaderwith state constantsEMPTY/LOADING/DONE(exposed both on the constructor and on instances, per WHATWG).FileReaderreadonly attributesreadyState,result,errorand theonloadstart/onprogress/onload/onabort/onerror/onloadendEventHandler attributes.FileReaderevents delivered via both theonXhandler slots andaddEventListener/removeEventListener/dispatchEvent.FileReader.readAsText/readAsArrayBuffer/readAsDataURL/abort.Implementation notes
Filedelegates its byte storage to the existingBlobpolyfill viaenv.Global().Get("Blob"), reusing Blob'sBlobParthandling (ArrayBuffer, typed array, string, Blob).Blob::Initializemust run beforeFile::Initialize.FileReader's readonly attributes (readyState/result/error) and theon*handler slots are backed by C++ state surfaced through prototype accessors, not by writable JS properties, so script can neither overwrite them nor fool the state-machine checks.result/errorare boxed on a persistent holder object rather thanNapi::Reference<Value>slots, becausenapi_create_referencerejects primitive values on the real N-API backends (V8/JSC) andreadAsText/readAsDataURLproduce primitive string results.readAsDataURLbase64-encodes via thebase-ndependency (added as a top-levelFetchContent), matching the encoder already used by other polyfills.FileReaderalive (per spec) by anchoring its JS wrapper in ashared_ptr<Napi::ObjectReference>captured by the promise reactions -- externally owned, released automatically when the promise settles. A monotonic read id letsabort()/ restart invalidate a stale continuation so a late-resolvingarrayBuffer()promise cannot dispatch a phantomloadafter a user-initiated abort.JSRUNTIMEHOST_POLYFILL_FILECMake option is on by default and gates building the target.Notable JSC-specific care
Two pieces of the implementation are shaped specifically to work correctly on JavaScriptCore:
FileReader.EMPTY/LOADING/DONEare registered viaStaticValueandInstanceValuedescriptors inside theDefineClassproperty list, not viafunc.Get("prototype").Set(...)after class creation. On JSC the napi shim'sConstructorInfo::Createdefaults the constructor's.prototypetoObject.prototype, so the latter pattern would polluteObject.prototypewithEMPTY/LOADING/DONEkeys and breakfor..inover plain objects throughout the runtime. The regression test"does not pollute Object.prototype with EMPTY/LOADING/DONE"pins this behaviour.The Blob-dependency guard in
File::InitializeusesIsUndefined()rather thanIsFunction(). Some JSC builds (notablylibjavascriptcoregtk-4.1on Linux) classify constructors created viaJSObjectMakeConstructorastypeof 'object', not'function', sonapi_typeofreturnsnapi_objectfor them and anIsFunction()guard would silently early-return on those engines.IsUndefined()matches the guard used by Blob and works on V8, JSI, Chakra, iOS-JSC, Android-JSC, and Linux JSC.Testing
Locally
UnitTests.exe(Win32 x64 Chakra Debug) passes all 180 JS tests (151 pre-existing + 29 new) and the native gtests, including theObject.prototype-pollution regression canary and the abort / late-resolve continuation guards. CI is green across all engines/platforms (V8, Chakra, JSI/JSC, iOS, Android, UWP, macOS, Ubuntu, sanitizers).After this merges
BabylonJS/BabylonNative#1706re-pinsCMakeLists.txt'sGIT_REPOSITORY/GIT_TAGto the merged SHA and removes the JS shim.